#!/usr/bin/python3
# ******************************************************************************
# Copyright (c) Huawei Technologies Co., Ltd. 2020-2020. All rights reserved.
# licensed under the Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
#     http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR
# PURPOSE.
# See the Mulan PSL v2 for more details.
# ******************************************************************************/
import os
from pathlib import Path
import sys
import re
import shutil
import yaml
import time
from xml.etree import ElementTree
from logger import logger
from api.build_env import OpenBuildService
from api.gitee import Gitee
from command import command
from conf import config

from constant import GIT_FETCH, MAINLINE_PROJECT_NAMES, OSC_CO_PATH
from exception import (
    BranchPackageError,
    CreateProjectError,
    DeletePackageError,
    ModifyProjectError,
    OscError,
    ProjectNameError,
)

from core import (
    ProcessRecords,
    ProjectMapping,
    extract_repo_pull,
    get_test_project_name,
)
from core.pull_link import Pull
from retrying import RetryError


#  retrying.RetryError  未捕获


def catch_error(func):
    def warp(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except (RetryError, AttributeError, IndexError, TypeError) as error:
            raise RuntimeError(f"An error occurred while building the package {error}")

    return warp


class BuildVerify:
    """
    软件包编译检查
    """

    # src_openeuler_ulr =  "https://gitee.com/jiangpengjuj"
    # src_openeuler_ulr = "https://gitee.com/gongzt"
    src_openeuler_ulr = "https://gitee.com/src-openeuler"

    def __init__(
        self,
        pull_request,
        target_branch,
        arch,
        multiple,
        ignore=False,
        account=None,
        password=None,
    ) -> None:
        self.account = account or config.build_env_account
        self.password = password or config.build_env_passwd
        self.api = OpenBuildService(account=self.account, password=self.password)
        self.pull = Pull()
        self.gitee = Gitee()
        self.test_branch = ""
        self.arch = arch
        self.p_project = ProjectMapping()
        self.origin_package, self.pr_num = extract_repo_pull(pull_request)
        self.target_branch = target_branch
        self.multiple = multiple
        self.ignore = ignore
        if not all([self.account, self.password, self.origin_package, self.pr_num]):
            raise RuntimeError(
                "Please check whether the path, account and password of obs are fully configured,\
                    Failed to get PR information"
            )

    @staticmethod
    def project_config(project_name):
        """
        读取project_config配置文件
        """
        try:
            yaml_path = (
                Path(__file__).parents[1].joinpath("conf", "project_config.yaml")
            )
            with open(yaml_path, encoding="utf-8") as file:
                content = yaml.load(file.read(), Loader=yaml.FullLoader)
                return content.get(project_name)
        except (FileNotFoundError, yaml.YAMLError) as error:
            logger.error(f"failed get project_config.yaml {error}")
            raise RuntimeError("data write yaml error")

    @staticmethod
    def _mkdir_osc_path():
        try:
            os.makedirs(OSC_CO_PATH, exist_ok=True)
        except PermissionError:
            raise OscError("Makedirs permission error: {OSC_CO_PATH}")

    def _get_packiage_service(self, find_patch, package):
        """
        Get the service of the original package
        """
        _ = os.path.isdir(os.path.join(OSC_CO_PATH, find_patch)) and shutil.rmtree(
            os.path.join(OSC_CO_PATH, find_patch)
        )
        cmds = ["osc", "co", find_patch, package]
        logger.info("osc co %s %s", find_patch, package)
        ret, _, _ = command(cmds, cwd=os.path.join(OSC_CO_PATH))

        if ret:
            raise OscError(
                f"Failed to get the service file of the package {package} under the {find_patch} branch"
            )

    def branch_package(self, find_repo, package):
        """
        Branch the parent project's package to the test project
        Args:
            find_repo: parent project name
            package: package name
        Raises:
             BranchPackageError: branch package error
        """

        branch_package_result = self.api.branch_package(
            project=find_repo, package=package, target_project=self.test_branch
        )
        if branch_package_result.get("status") == "failed":
            raise BranchPackageError(f"{package} branch error")

    def delete_project_package(self, project, package):
        """
        Remove packages from subprojects
        Args:
            project: project name
            package: package name

        Raises:
            DeletePackageError: failed to delete package in project
        """
        delete_package_result = self.api.del_project(project=project, package=package)
        if delete_package_result.get("status") == "failed":
            raise DeletePackageError(f"{package} delete error")

    def delete_package_from_project(self, project, exists_packages=list()):
        """
        Remove a specific package in a subproject
        Args:
            project: package name
            exists_packages: Packages that already exist in the project
        """
        packages = self.api.get_package_info(project=project)
        if exists_packages:
            packages = [
                relation_package
                for relation_package in exists_packages
                if relation_package in packages[:]
            ]
        for pkg in packages:
            if pkg:
                self.delete_project_package(project=project, package=pkg)

    def test_project_meta(self, find_branch, repositorys):
        """
        concatenated dictionary used to fill into the meta file of the project to create a new project
        Args:
            find_branch: project name
            repositorys: Archived repositories

        Returns:
            project_meta: Project meta configuration
        """
        project_meta = {
            "repository": [
                {
                    "path": repositorys
                    if repositorys
                    else {find_branch: f"standard_{self.arch}"},
                    "name": f"standard_{self.arch}",
                    "arch": self.arch,
                }
            ],
            "project": self.test_branch,
            "title": "Branch project for package",
            "description": "This project was created for package",
            "person": {"userid": self.api.account},
            "publish": "disable",
        }
        return project_meta

    def find_repo(self, package):
        """
        find the OBS branch that the REPO repository belongs to
        Args:
            package: pcakage name

        Returns:
            branch: branch name eg:"openEuler:Mainline"
        """
        obs_branch = self.p_project.branch_project(self.target_branch)
        for branch in obs_branch:
            if self.api.get_package_meta(branch, package):
                logger.info(f"This package {package} is under the project {branch}")
                return branch
        return False

    def query_project_state(self, project):
        """
        Query the build status of software packages every five seconds
        Args:
            project: project name

        Returns:
            project_results: Build information for all packages under the project
        """
        package_build_statuses = ["succeeded", "failed", "unresolvable", "excluded"]
        package_buid_result = ["building"]
        project_results = list()
        logger.info(
            f"http://117.78.1.88/project/show/{project} Package is building, please wait......."
        )
        while package_buid_result:
            time.sleep(5)
            project_results = self.api.get_project_build_state(project)
            package_buid_result = [
                package_result.get("result")
                for package_result in project_results
                if package_result.get("result") not in package_build_statuses
            ]
        package_build_results = list()
        for project_result in project_results[:]:
            # Query the build time of a package
            if project_result.get("result") == "excluded":
                logger.info(
                    f"package {project_result.get('package')} state is excluded"
                )
                build_time = 0
            else:
                build_time = self.api.package_build_time(
                    project,
                    project_result.get("arch"),
                    project_result.get("package"),
                    project_result.get("result"),
                )
            package_state = (
                "success"
                if project_result.get("result") == "succeeded"
                else "excluded"
                if project_result.get("result") == "excluded"
                else "failed"
            )

            project_result.update(build_time=build_time, result=package_state)
            package_build_results.append(project_result)
        logger.info(f"The result of the package build is: {package_build_results}")
        return package_build_results

    def get_project_repository(self, branch_meta):
        """
        Get the repository of the parent project
        Args:
            find_branch: project name

        Returns:
            repository: project repository
        """
        _repository = dict()
        for _res in (
            branch_meta.get("detail", dict())
            .get("repository", dict())
            .get(self.arch, list())
        ):
            _repository.update(_res)
        return _repository

    def epol_branch_map(self):
        """
        eopl branch map
        Returns:
            epol_branch_map: Corresponding epol branch name
        """
        return self.p_project.branch_project(self.target_branch)[0]

    def _put_project_config(self, find_branch, choose_all=False):
        """
        modify the configuration of the project
        Args:
            find_branch (_type_): _description_

        Raises:
            CreateProjectError: _description_

        """
        get_config = ""
        if choose_all:
            if self.target_branch == "master":
                get_config = self.project_config("openEuler:Epol")
            else:
                get_config = self.api.get_project_config(self.epol_branch_map())
        else:
            if self.target_branch == "master" and find_branch in MAINLINE_PROJECT_NAMES:
                get_config = self.api.get_project_config("openEuler:Mainline")
            elif self.target_branch == "master" and find_branch == "openEuler:Epol":
                get_config = self.project_config("openEuler:Epol")
            elif self.target_branch == "master" and find_branch == "openEuler:Factory":
                get_config = self.project_config("openEuler:Factory")
            else:
                get_config = self.api.get_project_config(find_branch)
        put_config = self.api.put_project_config(self.test_branch, get_config)
        if put_config.get("status") == "failed":
            raise CreateProjectError(
                f"Failed to modify the {find_branch} project configuration"
            )

    def _get_project_repositorys(self, find_branch, choose_all=False):
        if choose_all:
            if self.target_branch == "master":
                repositorys = self.p_project.project_repository(
                    "openEuler:Epol", self.arch
                )
            else:
                repositorys = self.get_project_repository(
                    self.api.project_meta(self.epol_branch_map())
                )
            return repositorys
        else:
            if self.target_branch == "master" and find_branch == "openEuler:Factory":
                repositorys = self.p_project.project_repository(find_branch, self.arch)
            elif self.target_branch == "master" and find_branch == "openEuler:Epol":
                repositorys = self.p_project.project_repository(find_branch, self.arch)
            elif (
                self.target_branch == "master" and find_branch in MAINLINE_PROJECT_NAMES
            ):
                repositorys = self.p_project.project_repository(
                    "openEuler:Mainline", self.arch
                )
            else:
                repositorys = self.get_project_repository(
                    self.api.project_meta(find_branch)
                )
            return repositorys

    def create_project(self, find_branch):
        """_summary_
        If the target project is found, the branch will be pulled from the target project.
        If not, the epoL project will be created under the TARGET Gitee project corresponding to the OBS project.
        After the branch is pulled, the warehouse will be deleted
        Args:
            find_branch (str): The target project name , or None if not found
        """
        find_branch = find_branch if find_branch else self.epol_branch_map()
        repositoryes = self._get_project_repositorys(find_branch)
        create_project_result = self.api.modify_project_meta(
            self.test_branch,
            self.test_project_meta(find_branch, repositoryes),
        )
        self._put_project_config(find_branch)
        self.delete_package_from_project(self.test_branch)
        return find_branch, create_project_result

    def copy_pr_osc(self, origin_package, branch_name):
        """
        Copy the new package to the local repo repository

        Args:
            origin_package: origin package name
            branch_name: Subproject name

        Returns:
           cp_code: cp operated code
           cp_error: cp operated error message
        """
        git_local_path = os.path.join(os.path.join(GIT_FETCH, origin_package))
        cp_code, _, cp_error = command(
            ["cp", "-r", git_local_path, os.path.join(OSC_CO_PATH, branch_name)]
        )
        return cp_code, cp_error

    def osc_add_package(self, origin_package, branch_name):
        """
        Upload a new package using the OSC
        Args:
            origin_package: The package name
            branch_name: Branch name

        Returns:
            ci_code, ci_error: osc upload result
        """
        osc_path = os.path.join(OSC_CO_PATH, branch_name)
        command(
            ["osc", "add", origin_package], cwd=os.path.join(OSC_CO_PATH, branch_name)
        )
        ci_code, _, ci_error = command(
            ["osc", "ci", "-m", f"new repo {origin_package} commit"], cwd=osc_path
        )
        return ci_code, ci_error

    def _handle_package_meta(self, find_branch, origin_package):
        """
        _service文件重组

        <services>
            <service name="tar_scm_kernel_repo">
                <param name="scm">repo</param>
                <param name="url">next/openEuler/perl-Archive-Zip</param>
            </service>
        </services>

        :param project: obs项目
        :param obs_work_dir: obs工作目录
        :param code_path: 代码目录
        :return:
        """
        _service_file_path = os.path.join(
            OSC_CO_PATH, find_branch, origin_package, "_service"
        )
        tree = ElementTree.parse(_service_file_path)

        logger.info("before update meta------")
        ElementTree.dump(tree)
        sys.stdout.flush()

        services = tree.findall("service")

        for service in services:
            if service.get("name") == "tar_scm_repo_docker":
                service.set("name", "tar_local")
            elif service.get("name") == "tar_scm_repo":
                service.set("name", "tar_local")
            elif service.get("name") == "tar_scm_kernel_repo":
                service.set("name", "tar_local_kernel")
            elif service.get("name") == "tar_scm_kernels_repo":
                service.set("name", "tar_local_kernels")
            elif service.get("name") == "tar_scm":
                service.set("name", "tar_local_kernel")

            for param in service.findall("param"):
                if param.get("name") == "scm":
                    param.text = "local"
                elif param.get("name") == "tar_scm":
                    param.text = "tar_local"
                elif param.get("name") == "url":
                    if (
                        "openEuler_kernel" in param.text
                        or "LTS_kernel" in param.text
                        or "openEuler-kernel" in param.text
                        or "openEuler-20.09_kernel" in param.text
                    ):
                        param.text = "{}/{}".format(
                            GIT_FETCH, "code"
                        )  # kernel special logical
                    else:
                        gitee_repo = re.sub(r"\.git", "", param.text.split("/")[-1])
                        param.text = "{}/{}".format(GIT_FETCH, gitee_repo)

        logger.info("after update meta------")

        ElementTree.dump(tree)
        sys.stdout.flush()
        tree.write(_service_file_path)

    def _prepare_build_environ(self, find_branch, package_name):
        """
        准备obs build环境
        :param project: obs项目
        :param obs_work_dir: obs工作目录
        :return:
        """
        _process_perl_path = os.path.join(
            os.path.dirname(__file__), "process_service.pl"
        )
        _service_file_path = os.path.join(
            OSC_CO_PATH, find_branch, package_name, "_service"
        )
        _git_package_path = os.path.join(GIT_FETCH, package_name)
        cmds = [
            "perl",
            _process_perl_path,
            "-f",
            _service_file_path,
            "-p",
            find_branch,
            "-m",
            package_name,
            "-w",
            _git_package_path,
        ]
        ret, _, _ = command(cmds=cmds)
        if ret:
            logger.error("prepare build environ error, %s", ret)
            raise OscError("prepare build environ error")

    def upload_pr_code_project(
        self, find_branch, src_openeuler_ulr, origin_package, pr_num, branch_name
    ):
        """
        Pull the new code, upload it to the test project, and query the compile status
        src_openeuler_ulr origin_package pr_num origin_package
        """
        self._mkdir_osc_path()
        self._get_packiage_service(find_branch, origin_package)
        logger.info("Start fetching newly submitted pr code, please wait.....")
        if origin_package == "kernel":
            self.pull.download_kernel_repo_of_tag(
                pr_num, f"{src_openeuler_ulr}/{origin_package}", origin_package
            )
        else:
            if not self.pull.fetch_pull(
                f"{src_openeuler_ulr}/{origin_package}", pr_num, origin_package
            ):
                raise BranchPackageError("failed to pull latest package")
        self._handle_package_meta(find_branch, origin_package)
        self._prepare_build_environ(find_branch, origin_package)
        co_code, _, co_error = command(["osc", "co", branch_name], cwd=OSC_CO_PATH)
        cp_code, cp_error = self.copy_pr_osc(origin_package, branch_name)
        osc_package_path = os.path.join(OSC_CO_PATH, branch_name, origin_package)
        _ = [
            shutil.rmtree(os.path.join(osc_package_path, files))
            for files in os.listdir(osc_package_path)[:]
            if os.path.isdir(os.path.join(osc_package_path, files))
        ]
        ci_code, ci_error = self.osc_add_package(origin_package, branch_name)
        _ = [
            os.path.isdir(path) and shutil.rmtree(path)
            for path in [OSC_CO_PATH, GIT_FETCH][:]
        ]
        if any([co_code, co_error, cp_code, cp_error, ci_code, ci_error]):
            raise OscError(
                f"An error occurred while using the osc command: {co_error} {cp_error} {ci_error}"
            )

    def get_relation_link(self, pr_number, repo, test_branch, exist_packages=list()):
        """
        Upload the related PR to the test project
        """
        relation_prs = list()
        relations = self.pull.relation_verify(pr_number, repo)
        for relation_pr in relations.get("be_link_pr", list()):
            if relation_pr["package"] in exist_packages:
                self.api.del_project(self.test_branch, relation_pr["package"])
            self.upload_pr_code_project(
                self.src_openeuler_ulr,
                relation_pr["package"],
                relation_pr["pull"],
                test_branch,
            )
            relation_prs.append(relation_pr["package"])
        return relation_prs

    def build_prep_single(self, find_branch):
        """_summary_
        single package build
        Args:
            single (_type_): _description_
        """
        # find_branch = self.find_repo(self.origin_package)
        self.test_branch = get_test_project_name(self.origin_package, self.pr_num)
        find_branch, create_project_result = self.create_project(find_branch)
        if create_project_result.get("status") == "failed":
            raise CreateProjectError(f"{self.test_branch} test project creation failed")
        self.upload_pr_code_project(
            find_branch,
            self.src_openeuler_ulr,
            self.origin_package,
            self.pr_num,
            self.test_branch,
        )
        # 单包编译结果查询
        package_build_results = self.query_project_state(self.test_branch)
        # 记录单包查询
        _ = [
            logger.info(f"package build results {package_build_result}")
            for package_build_result in package_build_results
        ]
        # 更改测试工程的配置 拿到所有的二进制归档
        if self.target_branch == "master" and find_branch != "openEuler:Factory":
            full_configuration_project = self.epol_branch_map()
            repositorys = self._get_project_repositorys(
                full_configuration_project, choose_all=True
            )
            # 修改了meta 这个软件包重新触发编译，耗时时间增长
            self.api.modify_project_meta(
                self.test_branch,
                self.test_project_meta(full_configuration_project, repositorys),
            )
            self._put_project_config(self.test_branch, choose_all=True)

        relation_prs = self.get_relation_link(
            self.pr_num, self.origin_package, self.test_branch
        )
        if relation_prs:
            return self.query_project_state(self.test_branch)
        return package_build_results

    def build_prep_multi(self, depend_list, find_branch):
        """
        A package pull process for multi-package build
        """
        self.test_branch = get_test_project_name(self.origin_package, self.pr_num)
        project_meta_response = self.api.project_meta(project=self.test_branch)
        if project_meta_response.get("status") == "failed":
            logger.error("fail to find test branch!")
            raise ProjectNameError(f"fail to find test branch!")
        exist_packages = self.api.get_package_info(project=self.test_branch)
        relation_prs = self.get_relation_link(
            pr_number=self.pr_num,
            repo=self.origin_package,
            test_branch=self.test_branch,
            exist_packages=exist_packages,
        )
        logger.info(f"{self.test_branch} packages: {exist_packages}")
        exist_packages.extend(relation_prs)
        for depend_pkg in depend_list[:]:
            if depend_pkg in exist_packages:
                continue
            find_repo = self.find_repo(depend_pkg)
            if not find_repo:
                logger.error(f"fail to find package {depend_pkg}")
                continue
            if (
                self.target_branch == "master"
                and find_branch != "openEuler:Factory"
                and find_repo == "openEuler:Factory"
            ):
                logger.info(
                    f"This dependent package {depend_pkg} is in openEuler:Factory, please analyze it yourself"
                )
                continue
            try:
                self.branch_package(find_repo=find_repo, package=depend_pkg)
            except BranchPackageError:
                logger.error(f"branch {find_repo} {depend_pkg} failed")
        package_states = self.query_project_state(self.test_branch)
        # 查询软件包上次的编译状态
        for package_state in package_states:
            if package_state.get("result") != "success":
                find_repo = self.find_repo(package_state.get("package"))
                if not find_repo or not self.api.build_package_lastlog(
                    find_repo,
                    package_state.get("package"),
                    package_state.get("arch"),
                ):
                    logger.error(
                        f"{package_state.get('package')}: The build result of the last package was"
                        f" not obtained, or the build result failed"
                    )
        return package_states

    def multi_data_combination(self, build_results):
        """_summary_"""
        package_build_results = dict()
        for sig_build_result in build_results:
            package_committer = self.gitee.package_committer(
                [sig_build_result.get("package")]
            )
            package_build_results.update(
                {
                    sig_build_result.get("package"): {
                        "build_result": sig_build_result.get("result"),
                        "log_url": sig_build_result.get("log_url"),
                        "commitor": package_committer.get(
                            sig_build_result.get("package")
                        ),
                    }
                }
            )
        return package_build_results

    @catch_error
    def build(self):
        """
        Package build
        a single source package or a list of multiple source packages
        Raises:
            ProjectNameError: Project Name Error

        Returns:
            result of build:
        """
        process_record = ProcessRecords(self.origin_package, self.pr_num)
        if not self.p_project.branch_project(self.target_branch):
            raise ProjectNameError(
                f"given branch name {self.target_branch} wrong,please check again!"
            )
        find_branch = self.find_repo(self.origin_package)
        if self.multiple:
            depend_list, _ = process_record.depend()
            build_results = self.build_prep_multi(depend_list, find_branch)
            package_build_results = self.multi_data_combination(build_results)
            steps = "multi_build_check"
        else:
            package_build_results = self.build_prep_single(find_branch)
            steps = "single_build_check"
        current_result = "success"
        result_field, package_build_resultes = (
            ("build_result", list(package_build_results.values()))
            if isinstance(package_build_results, dict)
            else ("result", package_build_results)
        )
        for package_build_result in package_build_resultes:
            if package_build_result.get(result_field) not in ["success", "excluded"]:
                current_result = "failed"
                break
        check_result = dict(
            build_detail=package_build_results, current_result=current_result
        )
        process_record.update_check_options(steps=steps, check_result=check_result)
        return check_result
